#!/usr/bin/python3
import re, serial
from motion_controllerui import MainUI
import threading, time

ports = {'crystal': 'COM7',
         'source': 'COM5',
         'detector': 'COM6',
         }
baudrate = 9600
timeout = 0.01
axes = ['x', 'y', 'z']
step = 2.5e-3

def serial_operation(func):
    def wrapper(port, *args, **kwargs):
        try:
            with serial.Serial(port, baudrate, timeout=timeout) as ser:
                return func(ser, *args, **kwargs)
        except:
            time.sleep(timeout)
            wrapper(port, *args, **kwargs)
    return wrapper

def ser_communicate(ser, command):
    ser.write(command.encode('utf-8'))
    resp = ser.readline().decode('utf-8')
    print(f'{ser.name} {command} {resp}')

@serial_operation
def read_position(ser):
    positions = []
    for axis in axes:
        ser.write(f'?{axis.capitalize()}/'.encode('utf-8'))
        resp = ser.readline().decode('utf-8')
        if resp:
            positions.append(round(int(re.findall('[+-]?\d+', resp)[0]) * step, 4))
        else:
            positions.append(0)
    return positions
    
@serial_operation
def move(ser, axis, dist, absolate=False):
    command = f'{axis.capitalize()}={dist/step:.0f}/' if absolate else f'{axis.capitalize()}:{dist/step:.0f}/'
    if re.search(':0/|:-0/', command):
        print(f'Skip {ser.name} {command}')
    else:
        ser_communicate(ser, command)

@serial_operation
def stop(ser):
    command = 'S0/'
    ser_communicate(ser, command)

@serial_operation
def reference(ser, mod='set'):
    for axis in axes:
        command = {'set': f'HM{axis.capitalize()}/',
                   'move': f'{axis.capitalize()}=0/'}
        ser_communicate(ser, command[mod])
            

class Main(MainUI):
    def __init__(self, master=None):
        super().__init__(master)

        self.running = True

        self.variables = {}
        for name in ports:
            for axis in axes:
                self.variables[f'{name}_{axis}_relative'] = self.builder.get_variable(f'{name}_{axis}_relative')
                self.variables[f'{name}_{axis}'] = self.builder.get_variable(f'{name}_{axis}')
                self.variables[f'{name}_{axis}_setter'] = self.builder.get_variable(f'{name}_{axis}_setter')

                self.builder.get_object(f'Button_{name}_{axis}_-')['command'] = lambda name=name, axis=axis: move(ports[name], axis, -self.variables[f'{name}_{axis}_relative'].get(), absolate=False)
                self.builder.get_object(f'Button_{name}_{axis}_+')['command'] = lambda name=name, axis=axis: move(ports[name], axis, self.variables[f'{name}_{axis}_relative'].get(), absolate=False)
                self.builder.get_object(f'Button_{name}_{axis}_set')['command'] = lambda name=name, axis=axis: move(ports[name], axis, self.variables[f'{name}_{axis}_setter'].get(), absolate=True)

        self.builder.get_object(f'Button_stop')['command'] = lambda: [stop(ports[name]) for name in ports]
        self.builder.get_object(f'Button_set_ref')['command'] = lambda: [reference(ports[name], mod='set') for name in ports]
        self.builder.get_object(f'Button_to_ref')['command'] = lambda: [reference(ports[name], mod='move') for name in ports]

        self.mainwindow.protocol('WM_DELETE_WINDOW', self.on_closing)
        threading.Thread(target=self.auto_read_positions, args=()).start()

    def auto_read_positions(self):
        while self.running:
            for name in ports:
                positions = read_position(ports[name])
                if positions and self.running:
                    for axis, position in zip(axes, positions):
                        self.variables[f'{name}_{axis}'].set(position)
            time.sleep(1)
            
    def on_closing(self):
        self.running = False
        self.mainwindow.destroy()

if __name__ == "__main__":
    app = Main()
    app.run()
